﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using StyleMVVM.Data.Activation;
using StyleMVVM.DependencyInjection;

namespace StyleMVVM.DotNet.Aspects.Impl
{
	public partial class AspectClassCreator
	{
		private static ConstructorInfo actionConstructorInfo;
		private static ConstructorInfo aspectContextConstrcutorInfo;
		private static MethodInfo proceedMethodInfo;
		private static MethodInfo getReturnValueMethodInfo;
		private static MethodInfo setReturnValueMethodInfo;
		private static MethodInfo containerLocateByTypeWithContextMethod;
		private static MethodInfo getTypeHandleMethod;
		private static MethodInfo getArgsMethod;
		private AspectClassInfo classInfo;
		private TypeBuilder typeBuilder;
		private Dictionary<Type, FieldInfo> sharedAspectFields;
		private List<Tuple<FieldInfo, AspectInfo>> knownAspects;

		static AspectClassCreator()
		{
			actionConstructorInfo = typeof(Action<>).MakeGenericType(typeof(IAspectInvokeContext)).GetConstructor(
														 BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
														 null,
														 new Type[] { typeof(Object), typeof(IntPtr) },
														 null);

			aspectContextConstrcutorInfo = typeof(AspectInvokeContext).GetConstructor(
														  BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
														  null,
														  new Type[]{
																typeof(String),
																typeof(Object),
																typeof(Object[]),
																typeof(IAspect[]),
																typeof(System.Action<>).MakeGenericType(typeof(IAspectInvokeContext))
																},
														  null
														  );

			proceedMethodInfo = typeof(AspectInvokeContext).GetMethod(
															"Proceed",
															BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
															null,
															new Type[] { },
															null);

			getReturnValueMethodInfo = typeof(AspectInvokeContext).GetMethod(
															"get_ReturnValue",
															BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
															null,
															new Type[] { },
															null);

			setReturnValueMethodInfo = typeof(AspectInvokeContext).GetMethod(
												"set_ReturnValue",
												BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
												null,
												new Type[] { typeof(Object) },
												null);

			containerLocateByTypeWithContextMethod = typeof(IDependencyInjectionContainer).GetMethod("LocateByTypeWithContext",
																													BindingFlags.Instance | BindingFlags.Public,
																													null,
																													new[]
																														{
																															typeof(IInjectionContext), 
																															typeof(Type), 
																															typeof(object[])
																														},
																													null);

			getTypeHandleMethod = typeof(Type).GetMethod("GetTypeFromHandle",
																		BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic,
																		null,
																		new[] { typeof(RuntimeTypeHandle) },
																		null
																		);

			getArgsMethod = typeof(IAspectInvokeContext).GetMethod("get_Args",
																					 BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
																					 null,
																					 new Type[] { },
																					 null);


		}

		public AspectClassCreator(ModuleBuilder moduleBuilder, AspectClassInfo classInfo)
		{
			knownAspects = new List<Tuple<FieldInfo, AspectInfo>>();
			sharedAspectFields = new Dictionary<Type, FieldInfo>();

			if (string.IsNullOrEmpty(classInfo.ClassName))
			{
				classInfo.ClassName = classInfo.ClassType.FullName + "_Proxy";
			}

			this.classInfo = classInfo;

			List<Type> interfaces = new List<Type> { typeof(ICustomInitialization) };

			if (classInfo.Interfaces != null)
			{
				interfaces.AddRange(classInfo.Interfaces);
			}

			typeBuilder = moduleBuilder.DefineType
													(classInfo.ClassName, TypeAttributes.Public, classInfo.ClassType, interfaces.ToArray());

		}

		public Type CreateType()
		{
			return InternalCreateType();
		}

		protected virtual Type InternalCreateType()
		{
			CreateAllConstructors();

			CreateOverrides();

			CreateCustomInitializer();

			return typeBuilder.CreateType();
		}

		private void CreateCustomInitializer()
		{
			System.Reflection.MethodAttributes methodAttributes = MethodAttributes.Public
																					 | MethodAttributes.Virtual
																					 | MethodAttributes.Final
																					 | MethodAttributes.HideBySig
																					 | MethodAttributes.NewSlot;

			MethodBuilder method = typeBuilder.DefineMethod("CustomInitialization", methodAttributes);

			method.SetReturnType(typeof(void));

			// Adding parameters
			method.SetParameters(typeof(IDependencyInjectionContainer), typeof(IInjectionContext));

			// Parameter container
			ParameterBuilder container = method.DefineParameter(1, ParameterAttributes.None, "container");

			// Parameter injectionContext
			ParameterBuilder injectionContext = method.DefineParameter(2, ParameterAttributes.None, "injectionContext");

			ILGenerator gen = method.GetILGenerator();

			foreach (Tuple<FieldInfo, AspectInfo> knownAspect in knownAspects)
			{
				Type aspectType = knownAspect.Item2.AspectType;

				if (typeof(IAspectShouldBeLocated).IsAssignableFrom(aspectType))
				{
					gen.Emit(OpCodes.Ldarg_0);
					gen.Emit(OpCodes.Ldarg_1);
					gen.Emit(OpCodes.Ldarg_2);
					gen.Emit(OpCodes.Ldtoken, aspectType);
					gen.Emit(OpCodes.Call, getTypeHandleMethod);
					gen.Emit(OpCodes.Ldnull);
					gen.Emit(OpCodes.Callvirt, containerLocateByTypeWithContextMethod);
					gen.Emit(OpCodes.Castclass, aspectType);
					gen.Emit(OpCodes.Stfld, knownAspect.Item1);
				}
				else
				{
					ConstructorInfo emptyConstructor = aspectType.GetConstructor(Type.EmptyTypes);

					gen.Emit(OpCodes.Ldarg_0);
					gen.Emit(OpCodes.Newobj, emptyConstructor);
					gen.Emit(OpCodes.Stfld, knownAspect.Item1);
				}
			}

			gen.Emit(OpCodes.Ret);
		}

		protected virtual void CreateAllConstructors()
		{

		}

		protected virtual void CreateOverrides()
		{
			List<AspectInfo> globalAspects = new List<AspectInfo>(classInfo.Aspects.FindAll(x => x.MemberInfo == null));

			foreach (IGrouping<MemberInfo, AspectInfo> aspectInfos in classInfo.Aspects.GroupBy(x => x.MemberInfo))
			{
				if (aspectInfos.Key == null)
				{
					continue;
				}

				List<AspectInfo> allAspects = new List<AspectInfo>(aspectInfos);

				allAspects.AddRange(globalAspects);
				allAspects.Sort((x, y) => Comparer<int>.Default.Compare(x.AspectPriority, y.AspectPriority));
				allAspects.Reverse();

				if (aspectInfos.Key is MethodInfo)
				{
					MethodInfo methodInfo = aspectInfos.Key as MethodInfo;

					if (methodInfo.IsGenericMethodDefinition)
					{
						CreateOpenGenericMethodOverride(aspectInfos.Key as MethodInfo, allAspects);
					}
					else
					{
						CreateNonGenericMethodOverride(aspectInfos.Key as MethodInfo, allAspects);
					}
				}
				else if (aspectInfos.Key is PropertyInfo)
				{
					CreatePropertyOverride(aspectInfos.Key as PropertyInfo, allAspects);
				}
			}
		}

		private void CreatePropertyOverride(PropertyInfo propertyInfo, List<AspectInfo> allAspects)
		{

		}

		protected virtual void CreateNonGenericMethodOverride(MethodInfo method, IEnumerable<AspectInfo> aspects)
		{
			MethodBuilder proceedMethod = CreatePrivateProceedMethod(method);

			System.Reflection.MethodAttributes methodAttributes =
								System.Reflection.MethodAttributes.Public
							 | System.Reflection.MethodAttributes.Virtual
							 | System.Reflection.MethodAttributes.HideBySig;

			MethodBuilder methodOverride = typeBuilder.DefineMethod(method.Name, methodAttributes);
			Type[] arguements = method.GetGenericArguments();

			methodOverride.SetReturnType(method.ReturnType);

			ParameterInfo[] parameterInfos = method.GetParameters();

			if (parameterInfos.Length > 0)
			{
				int count = 1;

				methodOverride.SetParameters(parameterInfos.Select(x => x.ParameterType).ToArray());

				foreach (ParameterInfo parameterInfo in parameterInfos)
				{
					methodOverride.DefineParameter(count, ParameterAttributes.None, parameterInfo.Name);

					count++;
				}
			}

			ILGenerator gen = methodOverride.GetILGenerator();

			CreateParameterObjectArray(gen, parameterInfos);

			CreateNewAspectsArray(gen, aspects);

			CreateAspectInvokeContext(method.Name, gen, proceedMethod);

			CreateProceedCall(gen);

			CreateOutAndReferenceStatements(gen, parameterInfos);

			CreateReturn(gen, method);
		}

		private void CreateReturn(ILGenerator gen, MethodInfo method)
		{
			if (method.ReturnType != typeof(void))
			{
				gen.Emit(OpCodes.Ldloc_2);

				gen.Emit(OpCodes.Callvirt, getReturnValueMethodInfo);

				if (method.ReturnType.IsValueType)
				{
					gen.Emit(OpCodes.Unbox_Any, method.ReturnType);
				}
			}

			gen.Emit(OpCodes.Ret);
		}

		private void CreateOutAndReferenceStatements(ILGenerator gen, ParameterInfo[] parameterInfos)
		{
			for (int i = 0; i < parameterInfos.Length; i++)
			{
				ParameterInfo parameterInfo = parameterInfos[i];

				if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef)
				{
					gen.Emit(OpCodes.Ldarg_S,(byte)( i + 1));
					gen.Emit(OpCodes.Ldloc_2);
					gen.Emit(OpCodes.Callvirt, getArgsMethod);
					gen.Emit(OpCodes.Ldc_I4_S, (byte)i);
					gen.Emit(OpCodes.Ldelem_Ref); 
					
					if (parameterInfo.ParameterType.GetElementType().IsValueType)
					{
						gen.Emit(OpCodes.Unbox_Any, parameterInfo.ParameterType.GetElementType());
					}
					else
					{
						gen.Emit(OpCodes.Castclass, parameterInfo.ParameterType.GetElementType());
					}

					gen.Emit(OpCodes.Stind_I4);
				}
			}
		}

		private void CreateAspectInvokeContext(string methodName, ILGenerator gen, MethodBuilder proceedMethod)
		{
			LocalBuilder aspectContext = gen.DeclareLocal(typeof(AspectInvokeContext));

			gen.Emit(OpCodes.Ldstr, methodName);
			gen.Emit(OpCodes.Ldarg_0);
			gen.Emit(OpCodes.Ldloc_0);
			gen.Emit(OpCodes.Ldloc_1);
			gen.Emit(OpCodes.Ldarg_0);
			gen.Emit(OpCodes.Ldftn, proceedMethod);
			gen.Emit(OpCodes.Newobj, actionConstructorInfo);
			gen.Emit(OpCodes.Newobj, aspectContextConstrcutorInfo);
			gen.Emit(OpCodes.Stloc_2);
		}

		private void CreateProceedCall(ILGenerator gen)
		{
			gen.Emit(OpCodes.Ldloc_2);
			gen.Emit(OpCodes.Callvirt, proceedMethodInfo);
		}

		private void CreateParameterObjectArray(ILGenerator gen, ParameterInfo[] parameterInfos)
		{
			LocalBuilder objArray = gen.DeclareLocal(typeof(Object[]));

			gen.Emit(OpCodes.Ldc_I4_S, (byte)parameterInfos.Length);
			gen.Emit(OpCodes.Newarr, typeof(object));
			gen.Emit(OpCodes.Stloc_0);

			for (int i = 0; i < parameterInfos.Length; i++)
			{
				ParameterInfo paramInfo = parameterInfos[i];

				if (paramInfo.IsOut)
				{
					continue;
				}

				gen.Emit(OpCodes.Ldloc_0);
				gen.Emit(OpCodes.Ldc_I4_S, (byte)i);
				gen.Emit(OpCodes.Ldarg_S, (byte)(i + 1));

				if (paramInfo.ParameterType.IsValueType)
				{
					gen.Emit(OpCodes.Box, paramInfo.ParameterType);
				}
				else if (paramInfo.ParameterType.IsByRef)
				{
					Type elementType = paramInfo.ParameterType.GetElementType();
					bool boxed = false;

					if (elementType.IsValueType)
					{
						if (elementType == typeof(sbyte))
						{
							gen.Emit(OpCodes.Ldind_I1);
							gen.Emit(OpCodes.Box, typeof(sbyte));
							boxed = true;
						}
						else if (elementType == typeof(byte))
						{
							gen.Emit(OpCodes.Ldind_U1);
							gen.Emit(OpCodes.Box, typeof(byte)); 
							boxed = true;
							
						}
						else if (elementType == typeof(short))
						{
							gen.Emit(OpCodes.Ldind_I2);
							gen.Emit(OpCodes.Box, typeof(short));
							boxed = true;
						}
						else if (elementType == typeof(ushort))
						{
							gen.Emit(OpCodes.Ldind_U2);
							gen.Emit(OpCodes.Box, typeof(ushort));
							boxed = true;
						}
						else if (elementType == typeof(int))
						{
							gen.Emit(OpCodes.Ldind_I4);
							gen.Emit(OpCodes.Box, typeof(int));
							boxed = true;
						}
						else if (elementType == typeof(uint))
						{
							gen.Emit(OpCodes.Ldind_U4);
							gen.Emit(OpCodes.Box, typeof(uint));
							boxed = true;
						}
						else if (elementType == typeof(float))
						{
							gen.Emit(OpCodes.Ldind_R4);
							gen.Emit(OpCodes.Box, typeof(float));
							boxed = true;
						}
						else if(elementType == typeof(double))
						{
							gen.Emit(OpCodes.Ldind_R8);
							gen.Emit(OpCodes.Box, typeof(double));
							boxed = true;
						}
					}

					if (!boxed)
					{
						gen.Emit(OpCodes.Ldind_Ref);
					}
				}

				gen.Emit(OpCodes.Stelem_Ref);
			}
		}

		private void CreateNewAspectsArray(ILGenerator gen, IEnumerable<AspectInfo> aspects)
		{
			LocalBuilder aspectsArray = gen.DeclareLocal(typeof(IAspect[]));

			gen.Emit(OpCodes.Ldc_I4_S, (byte)aspects.Count());
			gen.Emit(OpCodes.Newarr, typeof(IAspect));
			gen.Emit(OpCodes.Stloc_1);

			byte i = 0;
			foreach (AspectInfo aspectInfo in aspects)
			{
				FieldInfo aspectField = GetAspect(aspectInfo);

				gen.Emit(OpCodes.Ldloc_1);
				gen.Emit(OpCodes.Ldc_I4_S, i);
				gen.Emit(OpCodes.Ldarg_0);
				gen.Emit(OpCodes.Ldfld, aspectField);
				gen.Emit(OpCodes.Stelem_Ref);

				i++;
			}
		}

		protected MethodBuilder CreatePrivateProceedMethod(MethodInfo method)
		{
			MethodAttributes methodAttributes = MethodAttributes.Private | MethodAttributes.HideBySig;
			MethodBuilder proxyMethod = typeBuilder.DefineMethod("Proxy_" + method.Name, methodAttributes);

			// Setting return type
			proxyMethod.SetReturnType(typeof(void));

			// Adding parameters
			proxyMethod.SetParameters(typeof(IAspectInvokeContext));

			// Parameter invokeContext
			ParameterBuilder invokeContext = proxyMethod.DefineParameter(1, ParameterAttributes.None, "invokeContext");
			ParameterInfo[] parameterInfos = method.GetParameters();

			ILGenerator gen = proxyMethod.GetILGenerator();

			if (parameterInfos.Length == 0)
			{
				if (method.ReturnType != typeof(void))
				{
					gen.Emit(OpCodes.Ldarg_1);
				}

				// Writing body
				gen.Emit(OpCodes.Ldarg_0);
				gen.Emit(OpCodes.Call, method);
			}
			else
			{
				LocalBuilder objArray = gen.DeclareLocal(typeof(Object[]));

				gen.Emit(OpCodes.Ldarg_1);
				gen.Emit(OpCodes.Callvirt, getArgsMethod);
				gen.Emit(OpCodes.Stloc_0);

				int localCount = 1;
				for (int index = 0; index < parameterInfos.Length; index++)
				{
					ParameterInfo parameterInfo = parameterInfos[index];

					if (parameterInfo.IsOut)
					{
						gen.DeclareLocal(parameterInfo.ParameterType);
						localCount++;
					}
					else if (parameterInfo.ParameterType.IsByRef)
					{
						gen.DeclareLocal(parameterInfo.ParameterType);

						gen.Emit(OpCodes.Ldloc_0);
						gen.Emit(OpCodes.Ldc_I4_S, (byte)index);
						gen.Emit(OpCodes.Ldelem_Ref);

						if (parameterInfo.ParameterType.GetElementType().IsValueType)
						{
							gen.Emit(OpCodes.Unbox_Any, parameterInfo.ParameterType.GetElementType());
						}
						else
						{
							gen.Emit(OpCodes.Castclass, parameterInfo.ParameterType.GetElementType());
						}

						gen.Emit(OpCodes.Stloc_S, (byte)localCount);
						localCount++;
					}
				}

				if (method.ReturnType != typeof(void))
				{
					gen.Emit(OpCodes.Ldarg_1);
				}

				// setup stack to call base method
				gen.Emit(OpCodes.Ldarg_0);

				localCount = 1;

				for (int index = 0; index < parameterInfos.Length; index++)
				{
					ParameterInfo parameterInfo = parameterInfos[index];

					if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef)
					{
						gen.Emit(OpCodes.Ldloca_S, (byte)localCount);
						localCount++;
					}
					else
					{
						gen.Emit(OpCodes.Ldloc_0);
						gen.Emit(OpCodes.Ldc_I4_S, (byte)index);
						gen.Emit(OpCodes.Ldelem_Ref);

						if (parameterInfo.ParameterType.IsValueType)
						{
							gen.Emit(OpCodes.Unbox_Any, parameterInfo.ParameterType);
						}
						else
						{
							gen.Emit(OpCodes.Castclass, parameterInfo.ParameterType);
						}
					}
				}

				gen.Emit(OpCodes.Call, method);
			}

			if (method.ReturnType != typeof(void))
			{
				gen.Emit(OpCodes.Box, method.ReturnType);
				gen.Emit(OpCodes.Callvirt, setReturnValueMethodInfo);
			}

			int paramLocalCount = 1;
			for (int index = 0; index < parameterInfos.Length; index++)
			{
				ParameterInfo parameterInfo = parameterInfos[index];

				if (parameterInfo.IsOut || parameterInfo.ParameterType.IsByRef)
				{
					gen.Emit(OpCodes.Ldloc_0);
					gen.Emit(OpCodes.Ldc_I4_S, (byte)index);
					gen.Emit(OpCodes.Ldloc_S, (byte)paramLocalCount);

					if (parameterInfo.ParameterType.IsValueType)
					{
						gen.Emit(OpCodes.Box, parameterInfo.ParameterType);
					}
					else if (parameterInfo.ParameterType.IsByRef)
					{
						Type paramType = parameterInfo.ParameterType.GetElementType();

						if (paramType.IsValueType)
						{
							gen.Emit(OpCodes.Box, paramType);
						}
					}

					gen.Emit(OpCodes.Stelem_Ref);
					paramLocalCount++;
				}
			}

			gen.Emit(OpCodes.Ret);

			return proxyMethod;
		}

		protected virtual FieldInfo GetAspect(AspectInfo aspectInfo)
		{
			FieldInfo returnValue = null;

			if (sharedAspectFields.TryGetValue(aspectInfo.AspectType, out returnValue))
			{
				if (returnValue != null)
				{
					return returnValue;
				}

				returnValue = CreateNewField(aspectInfo.AspectType);
			}
			else
			{
				returnValue = CreateNewField(aspectInfo.AspectType);

				if (typeof(IAspectPerClass).GetTypeInfo().IsAssignableFrom(aspectInfo.AspectType))
				{
					sharedAspectFields.Add(aspectInfo.AspectType, returnValue);
				}
			}

			knownAspects.Add(new Tuple<FieldInfo, AspectInfo>(returnValue, aspectInfo));

			return returnValue;
		}

		protected virtual FieldInfo CreateNewField(Type fieldType)
		{
			FieldBuilder field = typeBuilder.DefineField(
				"_proxy_" + Guid.NewGuid().ToString("N"), fieldType, FieldAttributes.Private);

			return field;
		}
	}
}
